"""
test the pvgis IO tools
"""
import json
import numpy as np
import pandas as pd
import io
import pytest
import requests
from pvlib.iotools import get_pvgis_tmy, read_pvgis_tmy
from pvlib.iotools import get_pvgis_hourly, read_pvgis_hourly
from pvlib.iotools import get_pvgis_horizon
from tests.conftest import (
    TESTS_DATA_DIR,
    RERUNS,
    RERUNS_DELAY,
    assert_frame_equal,
    assert_series_equal,
)


# PVGIS Hourly tests
# The test files are actual files from PVGIS where the data section have been
# reduced to only a few lines
testfile_radiation_csv = TESTS_DATA_DIR / \
    'pvgis_hourly_Timeseries_45.000_8.000_SA_30deg_0deg_2016_2016.csv'
testfile_pv_json = TESTS_DATA_DIR / \
    'pvgis_hourly_Timeseries_45.000_8.000_SA2_10kWp_CIS_5_2a_2013_2014.json'

index_radiation_csv = \
    pd.date_range('20160101 00:10', freq='1h', periods=14, tz='UTC')
index_pv_json = \
    pd.date_range('2013-01-01 00:10', freq='1h', periods=10, tz='UTC')

columns_radiation_csv = [
    'Gb(i)', 'Gd(i)', 'Gr(i)', 'H_sun', 'T2m', 'WS10m', 'Int']
columns_radiation_csv_mapped = [
    'poa_direct', 'poa_sky_diffuse', 'poa_ground_diffuse', 'solar_elevation',
    'temp_air', 'wind_speed', 'Int']
columns_pv_json = [
    'P', 'G(i)', 'H_sun', 'T2m', 'WS10m', 'Int']
columns_pv_json_mapped = [
    'P', 'poa_global', 'solar_elevation', 'temp_air', 'wind_speed', 'Int']

data_radiation_csv = [
    [0.0, 0.0, 0.0, 0.0, 3.44, 1.43, 0.0],
    [0.0, 0.0, 0.0, 0.0, 2.94, 1.47, 0.0],
    [0.0, 0.0, 0.0, 0.0, 2.43, 1.51, 0.0],
    [0.0, 0.0, 0.0, 0.0, 1.93, 1.54, 0.0],
    [0.0, 0.0, 0.0, 0.0, 2.03, 1.62, 0.0],
    [0.0, 0.0, 0.0, 0.0, 2.14, 1.69, 0.0],
    [0.0, 0.0, 0.0, 0.0, 2.25, 1.77, 0.0],
    [0.0, 0.0, 0.0, 0.0, 3.06, 1.49, 0.0],
    [26.71, 8.28, 0.21, 8.06, 3.87, 1.22, 1.0],
    [14.69, 5.76, 0.16, 14.8, 4.67, 0.95, 1.0],
    [2.19, 0.94, 0.03, 19.54, 5.73, 0.77, 1.0],
    [2.11, 0.94, 0.03, 21.82, 6.79, 0.58, 1.0],
    [4.25, 1.88, 0.05, 21.41, 7.84, 0.4, 1.0],
    [0.0, 0.0, 0.0, 0.0, 7.43, 0.72, 0.0]]
data_pv_json = [
    [0.0, 0.0, 0.0, -0.97, 1.52, 0.0],
    [0.0, 0.0, 0.0, -1.06, 1.45, 0.0],
    [0.0, 0.0, 0.0, -1.03, 1.45, 0.0],
    [0.0, 0.0, 0.0, -0.48, 1.31, 0.0],
    [0.0, 0.0, 0.0, -0.09, 1.24, 0.0],
    [0.0, 0.0, 0.0, -0.38, 1.17, 0.0],
    [0.0, 0.0, 0.0, 0.29, 1.03, 0.0],
    [0.0, 0.0, 0.0, 1.0, 0.62, 0.0],
    [1187.2, 129.59, 8.06, 0.97, 0.97, 0.0],
    [3950.1, 423.28, 14.8, 1.89, 0.69, 0.0]]

inputs_radiation_csv = {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0,
                        'radiation_database': 'PVGIS-SARAH',
                        'Slope': '30 deg.', 'Azimuth': '0 deg.'}

metadata_radiation_csv = {
    'Gb(i)': 'Beam (direct) irradiance on the inclined plane (plane of the array) (W/m2)',  # noqa: E501
    'Gd(i)': 'Diffuse irradiance on the inclined plane (plane of the array) (W/m2)',  # noqa: E501
    'Gr(i)': 'Reflected irradiance on the inclined plane (plane of the array) (W/m2)',  # noqa: E501
    'H_sun': 'Sun height (degree)',
    'T2m': '2-m air temperature (degree Celsius)',
    'WS10m': '10-m total wind speed (m/s)',
    'Int': '1 means solar radiation values are reconstructed'}

inputs_pv_json = {
    'location': {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0},
    'meteo_data': {'radiation_db': 'PVGIS-SARAH2', 'meteo_db': 'ERA-Interim',
                   'year_min': 2013, 'year_max': 2014, 'use_horizon': True,
                   'horizon_db': None, 'horizon_data': 'DEM-calculated'},
    'mounting_system': {'two_axis': {
        'slope': {'value': '-', 'optimal': '-'},
        'azimuth': {'value': '-', 'optimal': '-'}}},
    'pv_module': {'technology': 'CIS', 'peak_power': 10.0, 'system_loss': 5.0}}


metadata_pv_json = {
    'inputs': {
        'location':
            {'description': 'Selected location', 'variables': {
                'latitude': {'description': 'Latitude', 'units': 'decimal degree'},  # noqa: E501
                'longitude': {'description': 'Longitude', 'units': 'decimal degree'},  # noqa: E501
                'elevation': {'description': 'Elevation', 'units': 'm'}}},
            'meteo_data': {
                'description': 'Sources of meteorological data',
                'variables': {
                    'radiation_db': {'description': 'Solar radiation database'},  # noqa: E501
                    'meteo_db': {'description': 'Database used for meteorological variables other than solar radiation'},  # noqa: E501
                    'year_min': {'description': 'First year of the calculations'},  # noqa: E501
                    'year_max': {'description': 'Last year of the calculations'},  # noqa: E501
                    'use_horizon': {'description': 'Include horizon shadows'},
                    'horizon_db': {'description': 'Source of horizon data'}}},
            'mounting_system': {
                'description': 'Mounting system',
                'choices': 'fixed, vertical_axis, inclined_axis, two_axis',
                'fields': {
                    'slope': {'description': 'Inclination angle from the horizontal plane', 'units': 'degree'},  # noqa: E501
                    'azimuth': {'description': 'Orientation (azimuth) angle of the (fixed) PV system (0 = S, 90 = W, -90 = E)', 'units': 'degree'}}},  # noqa: E501
            'pv_module': {
                'description': 'PV module parameters',
                'variables': {
                    'technology': {'description': 'PV technology'},
                    'peak_power': {'description': 'Nominal (peak) power of the PV module', 'units': 'kW'},  # noqa: E501
                    'system_loss': {'description': 'Sum of system losses', 'units': '%'}}}},  # noqa: E501
        'outputs': {
            'hourly': {
                'type': 'time series', 'timestamp': 'hourly averages',
                'variables': {
                    'P': {'description': 'PV system power', 'units': 'W'},
                    'G(i)': {'description': 'Global irradiance on the inclined plane (plane of the array)', 'units': 'W/m2'},  # noqa: E501
                    'H_sun': {'description': 'Sun height', 'units': 'degree'},
                    'T2m': {'description': '2-m air temperature', 'units': 'degree Celsius'},  # noqa: E501
                    'WS10m': {'description': '10-m total wind speed', 'units': 'm/s'},  # noqa: E501
                    'Int': {'description': '1 means solar radiation values are reconstructed'}}}}}  # noqa: E501

# Reformat the metadata as implemented in #2462
descriptions_csv = metadata_radiation_csv.copy()
metadata_radiation_csv = {}
metadata_radiation_csv['descriptions'] = descriptions_csv
metadata_radiation_csv['inputs'] = inputs_radiation_csv

descriptions_json = metadata_pv_json['inputs']
metadata_pv_json['inputs'] = inputs_pv_json
metadata_pv_json['inputs']['descriptions'] = descriptions_json


def generate_expected_dataframe(values, columns, index):
    """Create dataframe from arrays of values, columns and index, in order to
    use this dataframe to compare to.
    """
    expected = pd.DataFrame(index=index, data=values, columns=columns)
    expected['Int'] = expected['Int'].astype(int)
    expected.index.name = 'time'
    expected.index.freq = None
    return expected


@pytest.fixture
def expected_radiation_csv():
    expected = generate_expected_dataframe(
        data_radiation_csv, columns_radiation_csv, index_radiation_csv)
    return expected


@pytest.fixture
def expected_radiation_csv_mapped():
    expected = generate_expected_dataframe(
        data_radiation_csv, columns_radiation_csv_mapped, index_radiation_csv)
    return expected


@pytest.fixture
def expected_pv_json():
    expected = generate_expected_dataframe(
        data_pv_json, columns_pv_json, index_pv_json)
    return expected


@pytest.fixture
def expected_pv_json_mapped():
    expected = generate_expected_dataframe(
        data_pv_json, columns_pv_json_mapped, index_pv_json)
    return expected


# Test read_pvgis_hourly function using two different files with different
# input arguments (to test variable mapping and pvgis_format)
# pytest request.getfixturevalue is used to simplify the input arguments
@pytest.mark.parametrize('testfile,expected_name,metadata_exp,map_variables,pvgis_format', [  # noqa: E501
    (testfile_radiation_csv, 'expected_radiation_csv', metadata_radiation_csv,
     False, None),
    (testfile_radiation_csv, 'expected_radiation_csv_mapped',
     metadata_radiation_csv, True, 'csv'),
    (testfile_pv_json, 'expected_pv_json', metadata_pv_json,
     False, None),
    (testfile_pv_json, 'expected_pv_json_mapped', metadata_pv_json,
     True, 'json')])
def test_read_pvgis_hourly(testfile, expected_name, metadata_exp,
                           map_variables, pvgis_format, request):
    # Get expected dataframe from fixture
    expected = request.getfixturevalue(expected_name)
    # Read data from file
    out, metadata = read_pvgis_hourly(
        testfile, map_variables=map_variables, pvgis_format=pvgis_format)
    # Assert whether dataframe, metadata, and inputs are as expected
    assert_frame_equal(out, expected)
    assert metadata == metadata_exp


def test_read_pvgis_hourly_bad_extension():
    # Test if ValueError is raised if file extension cannot be recognized and
    # pvgis_format is not specified
    with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"):
        read_pvgis_hourly('filename.txt')
    # Test if ValueError is raised if an unkonwn pvgis_format is specified
    with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"):
        read_pvgis_hourly(testfile_pv_json, pvgis_format='txt')
    # Test if TypeError is raised if input is a buffer and pvgis_format=None.
    # The error text changed in python 3.12. This regex matches both versions:
    with pytest.raises(TypeError, match="str.*os.PathLike"):
        read_pvgis_hourly(io.StringIO())


args_radiation_csv = {
    'surface_tilt': 30, 'surface_azimuth': 180, 'outputformat': 'csv',
    'usehorizon': False, 'userhorizon': None, 'raddatabase': 'PVGIS-SARAH',
    'start': 2016, 'end': 2016, 'pvcalculation': False, 'components': True}

url_hourly_radiation_csv = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=csv&angle=30&aspect=0&usehorizon=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&raddatabase=PVGIS-SARAH&startyear=2016&endyear=2016'  # noqa: E501

args_pv_json = {
    'surface_tilt': 30, 'surface_azimuth': 180, 'outputformat': 'json',
    'usehorizon': True, 'userhorizon': None, 'raddatabase': 'PVGIS-SARAH2',
    'start': pd.Timestamp(2013, 1, 1), 'end': pd.Timestamp(2014, 5, 1),
    'pvcalculation': True, 'peakpower': 10, 'pvtechchoice': 'CIS', 'loss': 5,
    'trackingtype': 2, 'optimalangles': True, 'components': False,
    'url': 'https://re.jrc.ec.europa.eu/api/v5_2/'}

url_pv_json = 'https://re.jrc.ec.europa.eu/api/v5_2/seriescalc?lat=45&lon=8&outputformat=json&angle=30&aspect=0&pvtechchoice=CIS&mountingplace=free&trackingtype=2&components=0&usehorizon=1&raddatabase=PVGIS-SARAH2&startyear=2013&endyear=2014&pvcalculation=1&peakpower=10&loss=5&optimalangles=1'  # noqa: E501


@pytest.mark.parametrize('testfile,expected_name,args,map_variables,url_test', [  # noqa: E501
    (testfile_radiation_csv, 'expected_radiation_csv',
     args_radiation_csv, False, url_hourly_radiation_csv),
    (testfile_radiation_csv, 'expected_radiation_csv_mapped',
     args_radiation_csv, True, url_hourly_radiation_csv),
    (testfile_pv_json, 'expected_pv_json', args_pv_json, False, url_pv_json),
    (testfile_pv_json, 'expected_pv_json_mapped', args_pv_json, True,
     url_pv_json)])
def test_get_pvgis_hourly(requests_mock, testfile, expected_name, args,
                          map_variables, url_test, request):
    """Test that get_pvgis_hourly generates the correct URI request and that
    _parse_pvgis_hourly_json and _parse_pvgis_hourly_csv is called correctly"""
    # Open local test file containing McClear monthly data
    with open(testfile, 'r') as test_file:
        mock_response = test_file.read()
    # Specify the full URI of a specific example, this ensures that all of the
    # inputs are passing on correctly
    requests_mock.get(url_test, text=mock_response)
    # Make API call - an error is raised if requested URI does not match
    out, metadata = get_pvgis_hourly(
        latitude=45, longitude=8, map_variables=map_variables, **args)
    # Get expected dataframe from fixture
    expected = request.getfixturevalue(expected_name)
    # Compare out and expected dataframes
    assert_frame_equal(out, expected)


def test_get_pvgis_hourly_bad_status_code(requests_mock):
    # Test if a HTTPError is raised if a bad request is returned
    requests_mock.get(url_pv_json, status_code=400)
    with pytest.raises(requests.HTTPError):
        get_pvgis_hourly(latitude=45, longitude=8, **args_pv_json)
    # Test if HTTPError is raised and error message is returned if avaiable
    requests_mock.get(url_pv_json, status_code=400,
                      json={'message': 'peakpower Mandatory'})
    with pytest.raises(requests.HTTPError):
        get_pvgis_hourly(latitude=45, longitude=8, **args_pv_json)


url_bad_outputformat = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=basic&angle=0&aspect=0&pvcalculation=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=0&optimalinclination=0&loss=0'  # noqa: E501


def test_get_pvgis_hourly_bad_outputformat(requests_mock):
    # Test if a ValueError is raised if an unsupported outputformat is used
    # E.g. 'basic' is a valid PVGIS format, but is not supported by pvlib
    requests_mock.get(url_bad_outputformat)
    with pytest.raises(ValueError):
        get_pvgis_hourly(latitude=45, longitude=8, outputformat='basic')


url_additional_inputs = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=55.6814&lon=12.5758&outputformat=csv&angle=0&aspect=0&pvcalculation=1&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=1&optimalinclination=0&loss=2&userhorizon=10%2C15%2C20%2C10&peakpower=5'  # noqa: E501


def test_get_pvgis_hourly_additional_inputs(requests_mock):
    # Test additional inputs, including userhorizons
    # Necessary to pass a test file in order for the parser not to fail
    with open(testfile_radiation_csv, 'r') as test_file:
        mock_response = test_file.read()
    requests_mock.get(url_additional_inputs, text=mock_response)
    # Make request with userhorizon specified
    # Test passes if the request made by get_pvgis_hourly matches exactly the
    # url passed to the mock request (url_additional_inputs)
    get_pvgis_hourly(
        latitude=55.6814, longitude=12.5758, outputformat='csv',
        usehorizon=True, userhorizon=[10, 15, 20, 10], pvcalculation=True,
        peakpower=5, loss=2, trackingtype=0, components=True,
        optimalangles=True)


def test_read_pvgis_hourly_empty_file():
    # Check if a IOError is raised if file does not contain a data section
    with pytest.raises(ValueError, match='No data section'):
        read_pvgis_hourly(
            io.StringIO('1:1\n2:2\n3:3\n4:4\n5:5\n'),
            pvgis_format='csv')


# PVGIS TMY tests
@pytest.fixture
def expected():
    return pd.read_csv(
        TESTS_DATA_DIR / "pvgis_tmy_test.csv", index_col="time(UTC)"
    )


@pytest.fixture
def userhorizon_expected():
    return pd.read_json(TESTS_DATA_DIR / 'tmy_45.000_8.000_userhorizon.json')


@pytest.fixture
def month_year_expected():
    return [
        2018, 2007, 2009, 2013, 2008, 2006, 2011, 2010, 2020, 2006, 2007, 2016]


@pytest.fixture
def inputs_expected():
    return {
        'location': {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0,
                     'irradiance time offset': 0.1761},
        'meteo_data': {
            'radiation_db': 'PVGIS-SARAH3',
            'meteo_db': 'ERA5',
            'year_min': 2005,
            'year_max': 2023,
            'use_horizon': True,
            'horizon_db': 'DEM-calculated'}}


@pytest.fixture
def epw_meta():
    return {
        'loc': 'LOCATION',
        'city': 'unknown',
        'state-prov': '-',
        'country': 'unknown',
        'data_type': 'ECMWF/ERA',
        'WMO_code': 'unknown',
        'latitude': 45.0,
        'longitude': 8.0,
        'TZ': 1.0,
        'altitude': 250.0}


@pytest.fixture
def meta_expected():
    with (TESTS_DATA_DIR / 'pvgis_tmy_meta.json').open() as f:
        return json.load(f)


@pytest.fixture
def csv_meta(meta_expected):
    return meta_expected['outputs']['tmy_hourly']['variables']


@pytest.fixture
def pvgis_tmy_mapped_columns():
    return ['temp_air', 'relative_humidity', 'ghi', 'dni', 'dhi', 'IR(h)',
            'wind_speed', 'wind_direction', 'pressure']


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy(expected, month_year_expected, inputs_expected,
                       meta_expected):
    pvgis_data = get_pvgis_tmy(45, 8, map_variables=False, coerce_year=None)
    _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                            meta_expected, pvgis_data)


def _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                            meta_expected, pvgis_data):
    data, meta = pvgis_data

    # Re-create original outputs (prior to #2470)
    months_selected = meta['months_selected']
    inputs = meta['inputs'].copy()
    del inputs['descriptions']
    meta['inputs'] = meta['inputs']['descriptions']
    del meta['months_selected']

    # check each column of output separately
    for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys():
        assert np.allclose(data[outvar], expected[outvar])
    assert np.allclose(
        [_['month'] for _ in months_selected], np.arange(1, 13, 1))
    assert np.allclose(
        [_['year'] for _ in months_selected], month_year_expected)
    inputs_loc = inputs['location']
    assert inputs_loc['latitude'] == inputs_expected['location']['latitude']
    assert inputs_loc['longitude'] == inputs_expected['location']['longitude']
    assert inputs_loc['elevation'] == inputs_expected['location']['elevation']
    inputs_met_data = inputs['meteo_data']
    expected_met_data = inputs_expected['meteo_data']
    assert (
        inputs_met_data['radiation_db'] == expected_met_data['radiation_db'])
    assert inputs_met_data['year_min'] == expected_met_data['year_min']
    assert inputs_met_data['year_max'] == expected_met_data['year_max']
    assert inputs_met_data['use_horizon'] == expected_met_data['use_horizon']
    assert inputs_met_data['horizon_db'] == expected_met_data['horizon_db']
    assert meta == meta_expected


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_kwargs(userhorizon_expected):
    _, meta = get_pvgis_tmy(45, 8, usehorizon=False, map_variables=False)
    assert meta['inputs']['meteo_data']['use_horizon'] is False
    data, _ = get_pvgis_tmy(
        45, 8, userhorizon=[0, 10, 20, 30, 40, 15, 25, 5], map_variables=False,
        coerce_year=None)
    assert np.allclose(
        data['G(h)'], userhorizon_expected['G(h)'].values)
    assert np.allclose(
        data['Gb(n)'], userhorizon_expected['Gb(n)'].values)
    assert np.allclose(
        data['Gd(h)'], userhorizon_expected['Gd(h)'].values)
    _, meta = get_pvgis_tmy(45, 8, startyear=2005, map_variables=False)
    assert meta['inputs']['meteo_data']['year_min'] == 2005
    _, meta = get_pvgis_tmy(45, 8, endyear=2016, map_variables=False)
    assert meta['inputs']['meteo_data']['year_max'] == 2016


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_coerce_year():
    """test utc_offset and coerce_year work as expected"""
    base_case, _ = get_pvgis_tmy(45, 8)  # Turin
    assert str(base_case.index.tz) == 'UTC'
    assert base_case.index.name == 'time(UTC)'
    noon_test_data = [
        base_case[base_case.index.month == m].iloc[12]
        for m in range(1, 13)]
    cet_tz = 1  # Turin time is CET
    cet_name = 'Etc/GMT-1'
    # check indices of rolled data after converting timezone
    pvgis_data, _ = get_pvgis_tmy(45, 8, roll_utc_offset=cet_tz)
    jan1_midnight = pd.Timestamp('1990-01-01 00', tz=cet_name)
    dec31_midnight = pd.Timestamp('1990-12-31 23', tz=cet_name)
    assert pvgis_data.index[0] == jan1_midnight
    assert pvgis_data.index[-1] == dec31_midnight
    assert pvgis_data.index.name == f'time({cet_name})'
    # spot check rolled data matches original
    for m, test_case in enumerate(noon_test_data):
        expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz]
        assert all(test_case == expected)
    # repeat tests with year coerced
    test_yr = 2021
    pvgis_data, _ = get_pvgis_tmy(
        45, 8, roll_utc_offset=cet_tz, coerce_year=test_yr)
    jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00', tz=cet_name)
    dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23', tz=cet_name)
    assert pvgis_data.index[0] == jan1_midnight
    assert pvgis_data.index[-1] == dec31_midnight
    assert pvgis_data.index.name == f'time({cet_name})'
    for m, test_case in enumerate(noon_test_data):
        expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz]
        assert all(test_case == expected)
    # repeat tests with year coerced but utc offset none or zero
    pvgis_data, _ = get_pvgis_tmy(45, 8, coerce_year=test_yr)
    jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00', tz='UTC')
    dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23', tz='UTC')
    assert pvgis_data.index[0] == jan1_midnight
    assert pvgis_data.index[-1] == dec31_midnight
    assert pvgis_data.index.name == 'time(UTC)'
    for m, test_case in enumerate(noon_test_data):
        expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12]
        assert all(test_case == expected)
    # Test that get_pvgis_tmy defaults to coerce_year=1990
    pvgis_data, _ = get_pvgis_tmy(45, 8)
    assert all(pvgis_data.index.year == 1990)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                           meta_expected, csv_meta, coerce_year=None):
    pvgis_data = get_pvgis_tmy(45, 8, outputformat='csv', map_variables=False,
                               coerce_year=None)
    _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                           meta_expected, csv_meta, pvgis_data)


def _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                           meta_expected, csv_meta, pvgis_data):
    data, meta = pvgis_data

    # Re-create original outputs (prior to #2470)
    months_selected = meta['months_selected']
    inputs = meta['inputs'].copy()
    meta = meta['descriptions']

    # check each column of output separately
    for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys():
        assert np.allclose(data[outvar], expected[outvar])
    assert np.allclose(
        [_['month'] for _ in months_selected], np.arange(1, 13, 1))
    assert np.allclose(
        [_['year'] for _ in months_selected], month_year_expected)
    assert inputs['latitude'] == inputs_expected['location']['latitude']
    assert inputs['longitude'] == inputs_expected['location']['longitude']
    assert inputs['elevation'] == inputs_expected['location']['elevation']
    assert (inputs['irradiance time offset']
            == inputs_expected['location']['irradiance time offset']
            )
    for meta_value in meta:
        if not meta_value:
            continue
        # this copyright text tends to change (copyright year range increments
        # annually, e.g.), so just check the beginning of it:
        if meta_value.startswith('PVGIS (c) European'):
            continue
        assert meta_value in csv_meta


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_epw(expected, epw_meta):
    pvgis_data = get_pvgis_tmy(45, 8, outputformat='epw', map_variables=False,
                               coerce_year=None)
    _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)


def _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data):
    data, meta = pvgis_data
    assert np.allclose(data.ghi, expected['G(h)'])
    assert np.allclose(data.dni, expected['Gb(n)'])
    assert np.allclose(data.dhi, expected['Gd(h)'])
    assert np.allclose(data.temp_air, expected['T2m'])
    assert meta == epw_meta


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_error():
    err_msg = 'outputformat: Incorrect value.'
    with pytest.raises(requests.HTTPError, match=err_msg):
        get_pvgis_tmy(45, 8, outputformat='bad')
    with pytest.raises(requests.HTTPError, match='404 Client Error'):
        get_pvgis_tmy(45, 8, url='https://re.jrc.ec.europa.eu/')


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_basic():
    # Test that a specific error message is raised when outputformat='basic'
    err_msg = ("outputformat='basic' is no longer supported by pvlib, "
               "please use outputformat='csv' instead.")
    with pytest.raises(ValueError, match=err_msg):
        get_pvgis_tmy(45, 8, outputformat='basic')


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_get_pvgis_tmy_map_variables(pvgis_tmy_mapped_columns):
    actual, _ = get_pvgis_tmy(45, 8, map_variables=True)
    assert all(c in pvgis_tmy_mapped_columns for c in actual.columns)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_read_pvgis_horizon():
    pvgis_data, _ = get_pvgis_horizon(35.171051, -106.465158)
    horizon_data = pd.read_csv(TESTS_DATA_DIR / 'test_read_pvgis_horizon.csv',
                               index_col=0)
    horizon_data = horizon_data['horizon_elevation']
    assert_series_equal(pvgis_data, horizon_data)


@pytest.mark.remote_data
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
def test_read_pvgis_horizon_invalid_coords():
    with pytest.raises(requests.HTTPError, match='lat: Incorrect value'):
        _, _ = get_pvgis_horizon(100, 50)  # unfeasible latitude


def test_read_pvgis_tmy_map_variables(pvgis_tmy_mapped_columns):
    fn = TESTS_DATA_DIR / 'tmy_45.000_8.000_2005_2023.json'
    actual, _ = read_pvgis_tmy(fn, map_variables=True)
    assert all(c in pvgis_tmy_mapped_columns for c in actual.columns)


def test_read_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                             meta_expected):
    fn = TESTS_DATA_DIR / 'tmy_45.000_8.000_2005_2023.json'
    # infer outputformat from file extensions
    pvgis_data = read_pvgis_tmy(fn, map_variables=False)
    _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                            meta_expected, pvgis_data)
    # explicit pvgis outputformat
    pvgis_data = read_pvgis_tmy(fn, pvgis_format='json', map_variables=False)
    _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                            meta_expected, pvgis_data)
    with fn.open('r') as fbuf:
        pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='json',
                                    map_variables=False)
        _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
                                meta_expected, pvgis_data)


def test_read_pvgis_tmy_epw(expected, epw_meta):
    fn = TESTS_DATA_DIR / 'tmy_45.000_8.000_2005_2023.epw'
    # infer outputformat from file extensions
    pvgis_data = read_pvgis_tmy(fn, map_variables=False)
    _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
    # explicit pvgis outputformat
    pvgis_data = read_pvgis_tmy(fn, pvgis_format='epw', map_variables=False)
    _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
    with fn.open('r') as fbuf:
        pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='epw',
                                    map_variables=False)
        _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)


def test_read_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                            meta_expected, csv_meta):
    fn = TESTS_DATA_DIR / 'tmy_45.000_8.000_2005_2023.csv'
    # infer outputformat from file extensions
    pvgis_data = read_pvgis_tmy(fn, map_variables=False)
    _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                           meta_expected, csv_meta, pvgis_data)
    # explicit pvgis outputformat
    pvgis_data = read_pvgis_tmy(fn, pvgis_format='csv', map_variables=False)
    _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                           meta_expected, csv_meta, pvgis_data)
    with fn.open('rb') as fbuf:
        pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='csv',
                                    map_variables=False)
        _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
                               meta_expected, csv_meta, pvgis_data)


def test_read_pvgis_tmy_exception():
    bad_outputformat = 'bad'
    err_msg = f"pvgis format '{bad_outputformat:s}' was unknown"
    with pytest.raises(ValueError, match=err_msg):
        read_pvgis_tmy('filename', pvgis_format=bad_outputformat,
                       map_variables=False)


def test_read_pvgis_tmy_unknown_outputformat():
    with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"):
        read_pvgis_tmy("hello.txt")
